﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using OpenTK;
using OpenTK.Input;

using System.Runtime.InteropServices;

#if EMBEDDED
    using OpenTK.Graphics.ES20;
#else
using OpenTK.Graphics.OpenGL;
#endif

namespace MonoStrategy.RenderSystem
{
    public class TerrainMesh : IDisposable
    {
        private int m_VertexBufID = -1;
        private int m_IndexBufID = -1;
        private TerrainRenderer m_Parent;
        private bool[,] m_IsBlockValid;
        private float[] m_VertexBlock = new float[FloatsPerBlock];

        public const Int32 BlockSize = 16;
        private const Int32 FloatsPerBlock = BlockSize * BlockSize * 8;
        private const Int32 IndicesPerBlock = BlockSize * BlockSize * 6;
        private readonly Int32 IndicesPerLine;
        private readonly Int32 BlocksPerLine;

        public Int32 Size { get; private set; }
        public NativeTexture HeightMap { get; private set; }

        public TerrainMesh(TerrainRenderer inRenderer)
        {
            if (inRenderer == null)
                throw new ArgumentNullException();

            Size = inRenderer.Size;
            m_Parent = inRenderer;
            BlocksPerLine = (Size / BlockSize);
            IndicesPerLine = IndicesPerBlock * BlocksPerLine;
            m_IsBlockValid = new bool[BlocksPerLine, BlocksPerLine];

            if ((Size % BlockSize) != 0)
                throw new ArgumentOutOfRangeException("Terrain size is not a multiple of mesh block size.");

            GL.GenBuffers(1, out m_VertexBufID); GLRenderer.CheckError();
            GL.GenBuffers(1, out m_IndexBufID); GLRenderer.CheckError();

            // generate height grid
            float[] vertices = new float[Size * Size * 8];
            int[,] vertexIndices = new int[Size, Size];
            int offset = 0;

            for (int yBlock = 0, index = 0; yBlock < BlocksPerLine; yBlock++)
            {
                for (int xBlock = 0; xBlock < BlocksPerLine; xBlock++)
                {
                    for (int y = yBlock * BlockSize; y < (yBlock + 1) * BlockSize; y++)
                    {
                        for (int x = xBlock * BlockSize; x < (xBlock + 1) * BlockSize; x++)
                        {
                            vertexIndices[x, y] = index++;

                            /*
                             * Currently the normals are used for additional information like rivers, snow, etc. 
                             */
                            offset += 3;

                            vertices[offset++] = 0; // ground layer type
                            vertices[offset++] = 0; // graded or not?
                            vertices[offset++] = x;
                            vertices[offset++] = y;
                            vertices[offset++] = 0; // Z-Offset
                        }
                    }
                }
            }

            // generate indices for full terrain
            int[] indices = new int[IndicesPerLine * BlocksPerLine];
            offset = 0;

            for (int yBlock = 0; yBlock < BlocksPerLine; yBlock++)
            {
                for (int xBlock = 0; xBlock < BlocksPerLine; xBlock++)
                {
                    for (int y = yBlock * BlockSize; y < (yBlock + 1) * BlockSize; y++)
                    {
                        int iy = y;

                        if (y >= Size - 1)
                            iy = Size - 2;

                        for (int x = xBlock * BlockSize; x < (xBlock + 1) * BlockSize; x++)
                        {
                            int ix = x;

                            if (x >= Size - 1)
                                ix = Size - 2;

                            indices[offset++] = vertexIndices[ix, iy];
                            indices[offset++] = vertexIndices[ix + 1, iy];
                            indices[offset++] = vertexIndices[ix + 1, iy + 1];

                            indices[offset++] = vertexIndices[ix, iy];
                            indices[offset++] = vertexIndices[ix + 1, iy + 1];
                            indices[offset++] = vertexIndices[ix, iy + 1];
                        }
                    }
                }
            }


            if (offset != indices.Length)
                throw new ApplicationException();

            GL.BindBuffer(BufferTarget.ArrayBuffer, m_VertexBufID); GLRenderer.CheckError();
            GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(vertices.Length * 4), vertices, BufferUsageHint.DynamicDraw); GLRenderer.CheckError();

            GL.BindBuffer(BufferTarget.ElementArrayBuffer, m_IndexBufID); GLRenderer.CheckError();
            GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(indices.Length * 4), indices, BufferUsageHint.StaticDraw); GLRenderer.CheckError();

            HeightMap = new NativeTexture(TextureOptions.None, Size, Size, new int[Size * Size]);

            // listen to terrain changes
            m_Parent.Terrain.OnCellChanged += (cell) => { InvalidateCell(cell.X, cell.Y); };
        }

        ~TerrainMesh()
        {
            //if ((m_IndexBufID != -1) || (m_VertexBufID != -1))
            //    throw new ApplicationException("Terrain mesh was not released before GC.");
        }

        public void Dispose()
        {
            if (m_VertexBufID != -1)
                GL.DeleteBuffers(1, ref m_VertexBufID);

            if (m_IndexBufID != -1)
                GL.DeleteBuffers(1, ref m_IndexBufID);
            
            m_VertexBufID = -1;
            m_IndexBufID = -1;
        }

        /// <summary>
        /// Does a fast and accurate occlusion query for visible terrain cells.
        /// </summary>
        /// <returns></returns>
        public Rectangle ComputeOcclusion()
        {
            GL.ClearColor(Color.White);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            for (int y = 0; y <= Size; y += BlockSize)
            {
                for (int x = 0; x <= Size; x += BlockSize)
                {
                    GL.Begin(BeginMode.Quads);
                    {
                        float r = x / 1024.0f;
                        float g = y / 1024.0f;

                        GL.Color3(r, g, 0);
                        GL.Vertex3(x, y, 0.0);
                        GL.Color3(r, g, 0);
                        GL.Vertex3(x + BlockSize, y, 0.0);
                        GL.Color3(r, g, 0);
                        GL.Vertex3(x + BlockSize, y + BlockSize, 0.0);
                        GL.Color3(r, g, 0);
                        GL.Vertex3(x, y + BlockSize, 0.0);
                    }
                    GL.End();
                }
            }

            // detect frustum
            int[] buffer = new int[1];
            Point leftTop, rightTop, leftBottom, rightBottom;

            GL.ReadPixels(0, 0, 1, 1, PixelFormat.Rgb, PixelType.UnsignedByte, buffer); leftBottom = PixelToGrid(buffer[0]);
            GL.ReadPixels(m_Parent.Renderer.ViewportWidth - 1, 0, 1, 1, PixelFormat.Rgb, PixelType.UnsignedByte, buffer); rightBottom = PixelToGrid(buffer[0]);
            GL.ReadPixels(0, m_Parent.Renderer.ViewportHeight - 1, 1, 1, PixelFormat.Rgb, PixelType.UnsignedByte, buffer); leftTop = PixelToGrid(buffer[0]);
            GL.ReadPixels(m_Parent.Renderer.ViewportWidth - 1, m_Parent.Renderer.ViewportHeight - 1, 1, 1, PixelFormat.Rgb, PixelType.UnsignedByte, buffer); rightTop = PixelToGrid(buffer[0]);

            int minX = 0, maxX = Size, minY = 0, maxY = Size;

            if (leftTop.X >= 0)
            {
                minX = leftTop.X;
                minY = leftTop.Y;
            }

            if (rightTop.X >= 0)
            {
                maxX = rightTop.X;
                minY = rightTop.Y;
            }

            if (leftBottom.X >= 0)
            {
                minX = leftBottom.X;
                maxY = leftBottom.Y;
            }

            if (rightBottom.X >= 0)
            {
                maxX = rightBottom.X;
                maxY = rightBottom.Y;
            }


            if (!Program.Config.UseMinimalBounds)
            {
                minX = Math.Max(0, minX - BlockSize);
                minY = Math.Max(0, minY - BlockSize);
                maxX = Math.Min(Size, maxX + BlockSize);
                maxY = Math.Min(Size, maxY + BlockSize);
            }

            if ((leftTop.X < 0) && (rightTop.X < 0) && (leftBottom.X < 0) && (rightBottom.X < 0))
                return new Rectangle(0,0,0,0);    
            else
                return new Rectangle(minX, minY, maxX - minX + BlockSize, maxY - minY + BlockSize);
        }

        private Point PixelToGrid(int inPixel)
        {
            if ((inPixel & 0xFF0000) != 0)
                return new Point(-1, -1);
            else 
                return new Point((inPixel & 0xFF) * 4, ((inPixel & 0xFF00) >> 8) * 4);
        }

        /// <summary>
        /// Renders all blocks necessary to overlay the given rectangle of terrain cells.
        /// </summary>
        /// <param name="inContainedCells"></param>
        public void RenderBlocks(Rectangle inContainedCells, bool inCanUpdateBlocks)
        {
            Rectangle blocks = new Rectangle(
                Math.Max(0, Math.Min(inContainedCells.X / BlockSize, BlocksPerLine - 1)),
                Math.Max(0, Math.Min(inContainedCells.Y / BlockSize, BlocksPerLine - 1)),
                Math.Max(0, Math.Min((inContainedCells.X + inContainedCells.Width + BlockSize - 1) / BlockSize, BlocksPerLine)),
                Math.Max(0, Math.Min((inContainedCells.Y + inContainedCells.Height + BlockSize - 1) / BlockSize, BlocksPerLine)));

            blocks.Width = Math.Max(0, blocks.Width - blocks.X);
            blocks.Height = Math.Max(0, blocks.Height - blocks.Y);

            GL.BindBuffer(BufferTarget.ArrayBuffer, m_VertexBufID);
            GL.BindBuffer(BufferTarget.ElementArrayBuffer, m_IndexBufID);
            GL.InterleavedArrays(InterleavedArrayFormat.T2fN3fV3f, 0, IntPtr.Zero); GLRenderer.CheckError();

            /*
             * Blocks are aligned horizontally within the index buffer. Thus we can render all required blocks
             * on the same block line in one draw call.
             */
            for (int yBlock = blocks.Y, offset = IndicesPerLine * blocks.Y; yBlock < blocks.Y + blocks.Height; yBlock++, offset += IndicesPerLine)
            {
                int xBlock = blocks.X;
                int xCount = blocks.Width;

                if (inCanUpdateBlocks)
                {
                    for (int x = xBlock; x < xBlock + xCount; x++)
                    {
                        ValidateBlock(x, yBlock);
                    }
                }

                GL.DrawRangeElements(
                    BeginMode.Triangles,
                    0,
                    IndicesPerLine * BlocksPerLine,
                    xCount * IndicesPerBlock,
                    DrawElementsType.UnsignedInt,
                    (IntPtr)((offset + xBlock * IndicesPerBlock) * 4)); 
#if DEBUG
                GLRenderer.CheckError();
#endif
            }
        }

        /// <summary>
        /// Marks a given terrain cells as invalid, resulting in the underlying vertex
        /// block being updated on next rendering. This is a very fast operation!
        /// </summary>
        public void InvalidateCell(int inCellX, int inCellY)
        {
            /*
             * Doesn't need to be thread-safe since it doesn't matter if terrain gets updated
             * one frame sooner or later...
             */
            int xBlock = inCellX / BlockSize;
            int yBlock = inCellY / BlockSize;

            if ((xBlock >= BlocksPerLine) || (yBlock >= BlocksPerLine))
                return;

            m_IsBlockValid[xBlock, yBlock] = false;
        }

        /// <summary>
        /// Ensures that the given block is up to date and if not, fetches all required data from
        /// the renderer and propagates changes to GPU memory.
        /// </summary>
        private void ValidateBlock(int xBlock, int yBlock)
        {
            if ((xBlock >= BlocksPerLine) || (yBlock >= BlocksPerLine))
                return;

            if (m_IsBlockValid[xBlock, yBlock])
                return;

            var terrain = m_Parent.Terrain;
            int[] newHeights = new int[BlockSize * BlockSize];

            for (int y = yBlock * BlockSize, offset = 0, hOffset = 0; y < (yBlock + 1) * BlockSize; y++)
            {
                for (int x = xBlock * BlockSize; x < (xBlock + 1) * BlockSize; x++)
                {
                    offset += 3;

                    int height = terrain.GetHeightAt(x, y);

                    m_VertexBlock[offset++] = -1.0f + terrain.GetGroundAt(x, y) / 128.0f;
                    m_VertexBlock[offset++] = ((terrain.GetFlagsAt(x, y) & TerrainCellFlags.Grading) != 0) ? 1 : 0;
                    m_VertexBlock[offset++] = x;
                    m_VertexBlock[offset++] = y;
                    m_VertexBlock[offset++] = (-1.0f + height / 128.0f) / 2.0f;
                    newHeights[hOffset++] = height | (height << 8) | (height << 16) | (height << 24);
                }
            }

            HeightMap.SetPixels(xBlock * BlockSize, yBlock * BlockSize, BlockSize, BlockSize, newHeights);

            GL.BufferSubData(
                BufferTarget.ArrayBuffer,
                (IntPtr)(4 * FloatsPerBlock * (yBlock * BlocksPerLine + xBlock)),
                (IntPtr)(4 * FloatsPerBlock),
                m_VertexBlock);

#if DEBUG
            GLRenderer.CheckError();
#endif

            m_IsBlockValid[xBlock, yBlock] = true;
        }
    }
}
